Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update check_conditions() #1294

Merged
merged 18 commits into from
Sep 14, 2023
Merged

Update check_conditions() #1294

merged 18 commits into from
Sep 14, 2023

Conversation

mnwhite
Copy link
Contributor

@mnwhite mnwhite commented Jun 28, 2023

This PR incorporates CDC's work on the branch BST-HARK-pre-release-v4, at least with respect to the check_conditions() method for PerfForesightConsumerType and IndShockConsumerType. The new code structures in his branch has not been incorporated, as it represents a large overhaul of HARK. Instead, the condition-checking code has been greatly simplified, but also expanded to be more reader-friendly.

The check_conditions() method is now called automatically as part of pre_solve. This method fills in the attribute conditions_report, which is a string explaining parameter values, various patience factors (and their associated conditions), and an explanation of what the conditions mean jointly when verbose is True. A few of those messages need editing or verifying, so this PR is not yet ready to merge.

This PR also moves the calc_stable_points functionality out of the solver and into IndShockConsumerType. This change accelerates the solver by a factor of ~3.5x, and there was literally zero value to running calc_stable_points every single period during backward solution. The target (and balanced-growth) market resources ratio is not interesting, relevant, or meaningful outside of the infinite horizon, single repeated period case... and it doesn't mean anything until the solution has converged. This functionality is now run automatically as part of of post_solve when appropriate.

The other remaining work item is that all of the various factors that are computed are stored as attributes of the agent, which is not something we want to do anymore. These should be in a dictionary (like history or conditions), but I need feedback on what that dictionary should be called. Maybe factors? It's called Bilt in CDC's dev branch, but I'm not really a fan of that.

No new tests have been added, and I don't think they need to be. The changelog has not been updated, but needs to be.

  • Tests for new functionality/models or Tests to reproduce the bug-fix in code.
  • Updated documentation of features that add new functionality.
  • Update CHANGELOG.md with major/minor changes.

ConsIndShockSolver is always chosen now, and calc_bounding_values() works with new distribution format.
Perfect foresight model now produces a conditions_report field in addition to logging. Might need to check on the spacing with logging.
Not done yet. Also fixed a couple small mistakes in PF.
Still need to write "verbose" comments for check_conditions.
Still lacks Harmenberg growth patience factor/condition, as well as comments about the Modigliani mortality adjusted GIC. Will look at paper more closely.
There were a few typos. Violating FVAC but not WRIC now produces an ambiguous message, as the solution only *might* not exist.
Old add_stable_points methods in the PF and IndShock solvers were being run *every* period, which is very costly and essentially pointless. The new method lives on the IndShockConsumerType class and checks for relevant conditions before evaluating.

I need to double check, but I think the solution was accelerated significantly by getting rid of that code.
Search for mNrmTrg even if GICMod fails. Also put mNrmBal and mNrmTrg into top level. Need to change later. These changes were made while updating the BST dashboard notebook to be compatible.
The conditions_report is now sent to _log.info if not quiet. ConsIndShock.post_solve will now run calc_stable_points() if appropriate, so that the target and balanced-growth mNrm levels are added to the solution.

At the default parameters (infinite horizon), the main branch takes 0.328 seconds to solve on my computer. After moving the calc_stable_points code outside of the solver loop and only running it at the end (if appropriate), it takes 0.100 seconds to solve. Over two-thirds of solution time was being *wasted* on calc_stable_points, whose work is not used during the solution and *could not* plausibly be used.
@sbenthall
Copy link
Contributor

This PR makes me happy!

Would it be possible to make a separate PR for the movement of the stable points calculation?
It seems separable from the check_conditions parts.

My main comment on the check_conditions work, which is not meant to be a blocker, but rather as something more aspirational, is that really check_conditions depends on a lot of 'data'

  • mapping parameter names to parameter descriptions
  • mapping 'factor' or derivative variables more generally to the formula for computing them from more basic parameters
  • mapping conditions on those derivative variables to logging messages

I think that in an ideal world, none of this data would be hard-coded into the model. Rather, these can be configuration objects which are treated with generic modeling functionality.

I'm thinking of how to move in that direction in #1292 and would like to come up with some system we all feel good about.

@mnwhite
Copy link
Contributor Author

mnwhite commented Jun 28, 2023

check_conditions is very, very specific to the PF model and the ConsIndShock model. It could be plausibly extended to KinkedR and maybe ConsMarkovModel with more theory work, but it's a heavy lift to go beyond that. I don't think we want to have big code structures or concepts build around check_conditions, because we can only go so far with it-- it's not a general concept, in my opinion.

As for separating the changes to calc_stable_points from the check_conditions stuff... probably, but I'd need to undo work here. It looks like I put all of the "real" work for that into one commit, so maybe it's feasible.

Tests are failing right now because I renamed mNrmStE to mNrmBal. What does StE mean? Steady expectations? It's referred to as the "balanced growth" point in BST, so I changed it to Bal. I suppose I should keep the name for the sake of the tests, and then in a separate PR later change only the variable name.

@sbenthall
Copy link
Contributor

check_conditions is very, very specific to the PF model and the ConsIndShock model. It could be plausibly extended to KinkedR and maybe ConsMarkovModel with more theory work, but it's a heavy lift to go beyond that. I don't think we want to have big code structures or concepts build around check_conditions, because we can only go so far with it-- it's not a general concept, in my opinion.

The issue with the current architecture is that while these conditions are specific to the ConsIndShock model, because the ConsIndShock model is the superclass of all other consumption models, we wind up with very heavy, model-specific conditions code in every downstream model, even when it's entirely inappropriate. This is the worst of all worlds.

Making the condition checking code more generic improves that somewhat. Some aspects of the problem, such as wanting printable descriptions for model parameters, may be genuinely general.

But maybe you are right that checking conditions is so model specific that it should not be in the AgentType (or subclasses of it) code at all. In that case, I'd recommend that the conditions-checking functionality live in its own module, and take configuration data and the model as arguments.

@mnwhite
Copy link
Contributor Author

mnwhite commented Jun 28, 2023 via email

@sbenthall
Copy link
Contributor

But I don't think it should live outside of the AgentType subclasses. One of the improvements that should / will be made is to make details of the solution method depend on (at least some) of the conditions.

So, given a model $M$ parameterized by $p$, $M(p)$, there are some 'definitions' of mathematical objects that are significant, $q = d(p)$.

These values $q$ can have a variety of uses, include but not limited to

  • defining conditions to be checked before attempting to solve the model
  • informing points of extrapolation and/or kink points in the solution

What I've described is something general -- the ability to define derivative mathematical objects in terms of more basic parameters.

Dolo implements something like this generically, calling it auxiliary or I think definitions depending on where in the code you look:
https://dolo.readthedocs.io/en/latest/model_specification.html#auxiliary-variables

@sbenthall
Copy link
Contributor

sbenthall commented Jun 28, 2023

One part in particular that looks like useful general functionality that need not be hard-coded into a model is this part:
https://github.com/econ-ark/HARK/pull/1294/files#diff-7f09d28a3d4136ae35d8835cd1352f060da6e0114fe617c34374c8b57d41a57eR1814-R1822

I understand that you don't have the appetite for generalizing out this sort of functionality. I may get to it once the PR is merged. If you can keep that under consideration as you finish your implementation, that might make the transition towards separating models, solvers, and simulators from each other, which we discussed the other week, go smoother. As is, the check_conditions code is a point where the model definition and solver are tightly, and awkwardly, coupled.

Per SB's request, I'm splitting the change to calc_stable_points to be in a separate PR. This commit reverts the changes from a prior commit and should make the tests run properly now (because mNrmStE has not been renamed).
@mnwhite
Copy link
Contributor Author

mnwhite commented Jun 29, 2023

I just stripped out the changes to where "stable points" are calculated, so the tests should pass now. I'm 95% confident that the only failure mode was the renaming for mNrmStE to mNrmBal.

I still need to go double check some of the messages and make sure I didn't leave any "...I don't know what will happen, this is weird" stuff. The CHANGELOG should also be updated.

Oh, AND: @sbenthall What do you want to call the dictionary that holds auxiliary factors / information that never needs to be used by the solver, but is useful to have around? As-is, I put a lot more crap at the top level of the AgentType, but that should be fixed before merging.

@codecov
Copy link

codecov bot commented Jun 29, 2023

Codecov Report

Patch coverage: 82.51% and project coverage change: +0.11% 🎉

Comparison is base (7ce7138) 72.71% compared to head (2349984) 72.82%.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1294      +/-   ##
==========================================
+ Coverage   72.71%   72.82%   +0.11%     
==========================================
  Files          78       78              
  Lines       13057    13228     +171     
==========================================
+ Hits         9494     9633     +139     
- Misses       3563     3595      +32     
Files Changed Coverage Δ
HARK/core.py 87.67% <62.50%> (-0.73%) ⬇️
HARK/ConsumptionSaving/ConsIndShockModel.py 87.05% <83.70%> (-0.54%) ⬇️

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@sbenthall
Copy link
Contributor

Oh, AND: @sbenthall What do you want to call the dictionary that holds auxiliary factors / information that never needs to be used by the solver, but is useful to have around? As-is, I put a lot more crap at the top level of the AgentType, but that should be fixed before merging.

That's a good question. 'auxiliary' is not an obvious label to me. 'definitions'? Maybe @llorracc has an idea?

@sbenthall
Copy link
Contributor

@mnwhite status of this?

@mnwhite
Copy link
Contributor Author

mnwhite commented Jul 26, 2023 via email

A new dictionary has been added to PerfForesightConsumerType and IndShockConsumerType, called auxiliary. It contains all the various patience factors and other semi-useful values that are constructed for check_conditions, but aren't needed to solve (nor simulate) the model. This dictionary can be renamed with a simple replace-all on self.auxiliary.
Patient factors and conditions report now life in the bilt dictionary.
@mnwhite
Copy link
Contributor Author

mnwhite commented Aug 10, 2023

This PR is now complete, as far as I can tell @sbenthall . It looks like the Python 3.10 test might be timing out on MacOS only, but we'll see.

] # cycle time has already been advanced
# Cycle time has already been advanced
self.shocks["PermShk"] = PermGroFac[self.t_cycle - 1]
#self.shocks["PermShk"][self.t_cycle == 0] = 1. # Add this at some point
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused about this comment...

It looks like the main change here is just where the line breaks are.
If you use a code formatter like black does it revert this?

param_desc : str
Description of parameters as a unicode string.
'''
params_to_describe = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is nice

['PermGroFac', 'permanent income growth factor', 'G',True],
['CRRA', 'coefficient of relative risk aversion','ρ',False],
['LivPrb', 'survival probability','ℒ',True],
['APFac', 'absolute patience factor', 'Þ=(βℒR)^(1/ρ)',False]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

conceptually:

  • these are probably more like tuples than lists
  • you have an implicit type here: (string, string, string, bool)
  • this is really data about how the model is configured. Rather than putting it inside the method, it would be easier to (a) modify it and (b) change it in subclasses if this was stored in a data structure outside of the method.

@@ -1609,7 +1611,7 @@ def __init__(self, verbose=1, quiet=False, **kwds):
self.quiet = quiet
self.solve_one_period = make_one_period_oo_solver(ConsPerfForesightSolver)
set_verbosity_level((4 - verbose) * 10)

self.bilt = {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that in the "HARK 2.0 alpha" PR, CDC's use of mispelt English words was to make it easier to find-and-replace them all with a more readable, less opaque name.

Is this being used for anything besides conditions? I would be happy to name this conditions. Or could be built. Or derived.

for j in range(len(params_to_describe)):
this_entry = params_to_describe[j]
if this_entry[3]:
val = getattr(self,this_entry[0])[0]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that we now have an alternative way to get at a model's parameters.

I think we are currently storing the parameters dictionary as a dictionary, so that we can pull params from it, rather than relying on getattr to pull them from the object. The latter is the "whiteboard problem", and while this still works, I think we've implicitly deprecated this style.

So... consider self.parameters[this_entry[0]]

@@ -3071,120 +3163,239 @@ def pre_solve(self):
self.update_solution_terminal()
if not self.quiet:
self.check_conditions(verbose=self.verbose)


def describe_parameters(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confused about what this empty method is doing here.

val = getattr(self,this_entry[0])
except:
val = self.bilt[this_entry[0]]
this_line = this_entry[2] + f'={val:.5f} : ' + this_entry[1] + ' (' + this_entry[0] + ')\n'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way of constructing a string from these 'entries' is repeated often.
This can be functionalized out.

@sbenthall
Copy link
Contributor

Review is inline. Also see the merge conflict on the CHANGELOG.

With log utility, the expression for the utility-compensated permanent shock (and thus the value of autarky factor) would break with a divide by zero error. This is now fixed, with the end result that the VAFac is just beta with log utility.
@mnwhite
Copy link
Contributor Author

mnwhite commented Sep 14, 2023

I just fixed the last (very tiny) outstanding issue with this, so it's ready to merge, assuming checks pass. Exactly one check failed (for one version of Python?) on the previous commit, which only merged in recent changes from master. Not sure what's going on, but I've seen timeout issues before.

@mnwhite
Copy link
Contributor Author

mnwhite commented Sep 14, 2023

This is ready to merge @alanlujan91

@alanlujan91 alanlujan91 merged commit dce9901 into master Sep 14, 2023
19 checks passed
@mnwhite
Copy link
Contributor Author

mnwhite commented Sep 14, 2023

Ugh, I just realized that I missed / never saw all of @sbenthall 's review comments. Literally none of them, until just now when I got the notification that Alan merged it. To address them:

  1. Yes, I think that was mostly a whitespace change. I absolutely and utterly hate the way that black (or whatever we're using) formats code. It makes it much, much harder to read and massively inflates the linecount. If I added the commented out line with # add this at some point, it's because it looks like the simulator is grabbing from the wrong PermShk distribution when t_age = 0. This is because our current format doesn't define the income distribution in the very first period of life.

  2. No other classes will be getting similar treatment, so there's no point in making a general code structure for this. I just did that for convenience.

  3. CDC has dictated that it shall be called bilt for now.

  4. I don't see any empty method there; pre_solve just calls update_solution_terminal, like usual, and it tries to check_conditions if relevant.

  5. I think I used that string combining functionality twice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants